package edu.northwestern.cbits.purple_robot_manager.probes.builtin;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.CheckBoxPreference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import edu.northwestern.cbits.purple_robot_manager.R;
import edu.northwestern.cbits.purple_robot_manager.activities.WebkitActivity;
import edu.northwestern.cbits.purple_robot_manager.activities.WebkitLandscapeActivity;
import edu.northwestern.cbits.purple_robot_manager.activities.settings.FlexibleListPreference;
import edu.northwestern.cbits.purple_robot_manager.charts.SplineChart;
import edu.northwestern.cbits.purple_robot_manager.db.ProbeValuesProvider;
import edu.northwestern.cbits.purple_robot_manager.logging.LogManager;
import edu.northwestern.cbits.purple_robot_manager.probes.Probe;
@SuppressLint("SimpleDateFormat")
public class GravityProbe extends Continuous3DProbe implements SensorEventListener
{
private static int BUFFER_SIZE = 1024;
public static final String DB_TABLE = "gravity_probe";
private static final String[] fieldNames = { Continuous3DProbe.X_KEY,
Continuous3DProbe.Y_KEY,
Continuous3DProbe.Z_KEY };
private static final String DEFAULT_THRESHOLD = "0.5";
public static final String NAME = "edu.northwestern.cbits.purple_robot_manager.probes.builtin.GravityProbe";
private static final String ENABLED = "config_probe_gravity_built_in_enabled";
private static final String FREQUENCY = "config_probe_gravity_built_in_frequency";
private static final String THRESHOLD = "config_probe_gravity_built_in_threshold";
private static final String USE_HANDLER = "config_probe_gravity_built_in_handler";
private long lastThresholdLookup = 0;
private double lastThreshold = 0.5;
private double _lastX = Double.MAX_VALUE;
private double _lastY = Double.MAX_VALUE;
private double _lastZ = Double.MAX_VALUE;
private final float valueBuffer[][] = new float[3][BUFFER_SIZE];
private final int accuracyBuffer[] = new int[BUFFER_SIZE];
private final double timeBuffer[] = new double[BUFFER_SIZE];
private final double sensorTimeBuffer[] = new double[BUFFER_SIZE];
private Map<String, String> _schema = null;
private int _lastFrequency = -1;
private int bufferIndex = 0;
private static Handler _handler = null;
@Override
public boolean getUsesThread()
{
SharedPreferences prefs = ContinuousProbe.getPreferences(this._context);
return prefs.getBoolean(GravityProbe.USE_HANDLER, GravityProbe.DEFAULT_USE_HANDLER);
}
@Override
public Intent viewIntent(Context context)
{
Intent i = new Intent(context, WebkitLandscapeActivity.class);
return i;
}
@Override
protected String tableName()
{
return GravityProbe.DB_TABLE;
}
@Override
public String probeCategory(Context context)
{
return context.getString(R.string.probe_sensor_category);
}
@Override
public String contentSubtitle(Context context)
{
Cursor c = ProbeValuesProvider.getProvider(context).retrieveValues(context, GravityProbe.DB_TABLE, this.databaseSchema());
int count = -1;
if (c != null)
{
count = c.getCount();
c.close();
}
return String.format(context.getString(R.string.display_item_count), count);
}
public Map<String, String> databaseSchema()
{
if (this._schema == null)
{
this._schema = new HashMap<>();
this._schema.put(Continuous3DProbe.X_KEY, ProbeValuesProvider.REAL_TYPE);
this._schema.put(Continuous3DProbe.Y_KEY, ProbeValuesProvider.REAL_TYPE);
this._schema.put(Continuous3DProbe.Z_KEY, ProbeValuesProvider.REAL_TYPE);
}
return this._schema;
}
@Override
public String getDisplayContent(Activity activity)
{
try
{
String template = WebkitActivity.stringForAsset(activity, "webkit/chart_spline_full.html");
ArrayList<Double> x = new ArrayList<>();
ArrayList<Double> y = new ArrayList<>();
ArrayList<Double> z = new ArrayList<>();
ArrayList<Double> time = new ArrayList<>();
Cursor cursor = ProbeValuesProvider.getProvider(activity).retrieveValues(activity, GravityProbe.DB_TABLE, this.databaseSchema());
int count = -1;
if (cursor != null)
{
count = cursor.getCount();
while (cursor.moveToNext())
{
double xd = cursor.getDouble(cursor.getColumnIndex(Continuous3DProbe.X_KEY));
double yd = cursor.getDouble(cursor.getColumnIndex(Continuous3DProbe.Y_KEY));
double zd = cursor.getDouble(cursor.getColumnIndex(Continuous3DProbe.Z_KEY));
double t = cursor.getDouble(cursor.getColumnIndex(ProbeValuesProvider.TIMESTAMP));
x.add(xd);
y.add(yd);
z.add(zd);
time.add(t);
}
cursor.close();
}
SplineChart c = new SplineChart();
c.addSeries("X", x);
c.addSeries("Y", y);
c.addSeries("Z", z);
c.addTime("tIME", time);
JSONObject json = c.dataJson(activity);
template = template.replace("{{{ highchart_json }}}", json.toString());
template = template.replace("{{{ highchart_count }}}", "" + count);
return template;
}
catch (IOException | JSONException e)
{
LogManager.getInstance(activity).logException(e);
}
return null;
}
@Override
public Bundle formattedBundle(Context context, Bundle bundle)
{
Bundle formatted = super.formattedBundle(context, bundle);
double[] eventTimes = bundle.getDoubleArray(ContinuousProbe.EVENT_TIMESTAMP);
double[] x = bundle.getDoubleArray(Continuous3DProbe.X_KEY);
double[] y = bundle.getDoubleArray(Continuous3DProbe.Y_KEY);
double[] z = bundle.getDoubleArray(Continuous3DProbe.Z_KEY);
ArrayList<String> keys = new ArrayList<>();
SimpleDateFormat sdf = new SimpleDateFormat(context.getString(R.string.display_date_format));
if (x == null || y == null || z == null)
{
}
else if (eventTimes.length > 1)
{
Bundle readings = new Bundle();
for (int i = 0; i < eventTimes.length; i++)
{
String formatString = String.format(context.getString(R.string.display_gyroscope_reading), x[i], y[i], z[i]);
double time = eventTimes[i];
Date d = new Date((long) time);
String key = sdf.format(d);
readings.putString(key, formatString);
keys.add(key);
}
if (keys.size() > 0)
readings.putStringArrayList("KEY_ORDER", keys);
formatted.putBundle(context.getString(R.string.display_gravity_readings), readings);
}
else if (eventTimes.length > 0)
{
String formatString = String.format(context.getString(R.string.display_gyroscope_reading), x[0], y[0], z[0]);
double time = eventTimes[0];
Date d = new Date((long) time);
formatted.putString(sdf.format(d), formatString);
}
return formatted;
}
@Override
public long getFrequency()
{
SharedPreferences prefs = ContinuousProbe.getPreferences(this._context);
return Long.parseLong(prefs.getString(GravityProbe.FREQUENCY, ContinuousProbe.DEFAULT_FREQUENCY));
}
@Override
public String name(Context context)
{
return GravityProbe.NAME;
}
@Override
public int getTitleResource()
{
return R.string.title_gravity_probe;
}
@Override
@SuppressLint("InlinedApi")
public boolean isEnabled(Context context)
{
if (Build.VERSION.SDK_INT < 9)
return false;
SharedPreferences prefs = ContinuousProbe.getPreferences(context);
this._context = context.getApplicationContext();
final SensorManager sensors = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
final Sensor sensor = sensors.getDefaultSensor(Sensor.TYPE_GRAVITY);
if (super.isEnabled(context))
{
if (prefs.getBoolean(GravityProbe.ENABLED, ContinuousProbe.DEFAULT_ENABLED))
{
int frequency = Integer.parseInt(prefs.getString(GravityProbe.FREQUENCY, ContinuousProbe.DEFAULT_FREQUENCY));
if (this._lastFrequency != frequency)
{
sensors.unregisterListener(this, sensor);
if (GravityProbe._handler != null)
{
Looper loop = GravityProbe._handler.getLooper();
loop.quit();
GravityProbe._handler = null;
}
if (frequency != SensorManager.SENSOR_DELAY_FASTEST && frequency != SensorManager.SENSOR_DELAY_UI &&
frequency != SensorManager.SENSOR_DELAY_NORMAL)
{
frequency = SensorManager.SENSOR_DELAY_GAME;
}
if (prefs.getBoolean(GravityProbe.USE_HANDLER, ContinuousProbe.DEFAULT_USE_HANDLER))
{
final GravityProbe me = this;
final int finalFrequency = frequency;
Runnable r = new Runnable()
{
public void run()
{
Looper.prepare();
GravityProbe._handler = new Handler();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
sensors.registerListener(me, sensor, finalFrequency, 0, GravityProbe._handler);
else
sensors.registerListener(me, sensor, finalFrequency, GravityProbe._handler);
Looper.loop();
}
};
Thread t = new Thread(r, "gravity");
t.start();
}
else
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
sensors.registerListener(this, sensor, frequency, 0);
else
sensors.registerListener(this, sensor, frequency, null);
}
this._lastFrequency = frequency;
}
return true;
}
else
{
sensors.unregisterListener(this, sensor);
this._lastFrequency = -1;
if (GravityProbe._handler != null)
{
Looper loop = GravityProbe._handler.getLooper();
loop.quit();
GravityProbe._handler = null;
}
}
}
else
{
sensors.unregisterListener(this, sensor);
this._lastFrequency = -1;
if (GravityProbe._handler != null)
{
Looper loop = GravityProbe._handler.getLooper();
loop.quit();
GravityProbe._handler = null;
}
}
return false;
}
@Override
protected boolean passesThreshold(SensorEvent event)
{
long now = System.currentTimeMillis();
if (now - this.lastThresholdLookup > 5000)
{
this.lastThreshold = this.getThreshold();
this.lastThresholdLookup = now;
}
double x = event.values[0];
double y = event.values[1];
double z = event.values[2];
boolean passes = false;
if (Math.abs(x - this._lastX) >= this.lastThreshold)
passes = true;
else if (Math.abs(y - this._lastY) >= this.lastThreshold)
passes = true;
else if (Math.abs(z - this._lastZ) >= this.lastThreshold)
passes = true;
if (passes)
{
this._lastX = x;
this._lastY = y;
this._lastZ = z;
}
return passes;
}
@Override
public PreferenceScreen preferenceScreen(Context context, PreferenceManager manager)
{
PreferenceScreen screen = super.preferenceScreen(context, manager);
FlexibleListPreference threshold = new FlexibleListPreference(context);
threshold.setKey(GravityProbe.THRESHOLD);
threshold.setDefaultValue(GravityProbe.DEFAULT_THRESHOLD);
threshold.setEntryValues(R.array.probe_accelerometer_threshold);
threshold.setEntries(R.array.probe_accelerometer_threshold_labels);
threshold.setTitle(R.string.probe_noise_threshold_label);
threshold.setSummary(R.string.probe_noise_threshold_summary);
screen.addPreference(threshold);
CheckBoxPreference handler = new CheckBoxPreference(context);
handler.setTitle(R.string.title_own_sensor_handler);
handler.setKey(GravityProbe.USE_HANDLER);
handler.setDefaultValue(ContinuousProbe.DEFAULT_USE_HANDLER);
screen.addPreference(handler);
return screen;
}
@Override
public void onSensorChanged(SensorEvent event)
{
final double now = (double) System.currentTimeMillis();
if (this.shouldProcessEvent(event) == false)
return;
if (this.passesThreshold(event))
{
synchronized (this)
{
sensorTimeBuffer[bufferIndex] = event.timestamp;
timeBuffer[bufferIndex] = now / 1000;
accuracyBuffer[bufferIndex] = event.accuracy;
for (int i = 0; i < event.values.length; i++)
{
valueBuffer[i][bufferIndex] = event.values[i];
}
bufferIndex += 1;
if (bufferIndex >= timeBuffer.length)
{
Sensor sensor = event.sensor;
Bundle data = new Bundle();
Bundle sensorBundle = new Bundle();
sensorBundle.putFloat(ContinuousProbe.SENSOR_MAXIMUM_RANGE, sensor.getMaximumRange());
sensorBundle.putString(ContinuousProbe.SENSOR_NAME, sensor.getName());
sensorBundle.putFloat(ContinuousProbe.SENSOR_POWER, sensor.getPower());
sensorBundle.putFloat(ContinuousProbe.SENSOR_RESOLUTION, sensor.getResolution());
sensorBundle.putInt(ContinuousProbe.SENSOR_TYPE, sensor.getType());
sensorBundle.putString(ContinuousProbe.SENSOR_VENDOR, sensor.getVendor());
sensorBundle.putInt(ContinuousProbe.SENSOR_VERSION, sensor.getVersion());
data.putDouble(Probe.BUNDLE_TIMESTAMP, now / 1000);
data.putString(Probe.BUNDLE_PROBE, this.name(this._context));
data.putBundle(ContinuousProbe.BUNDLE_SENSOR, sensorBundle);
data.putDoubleArray(ContinuousProbe.EVENT_TIMESTAMP, timeBuffer);
data.putDoubleArray(ContinuousProbe.SENSOR_TIMESTAMP, sensorTimeBuffer);
data.putIntArray(ContinuousProbe.SENSOR_ACCURACY, accuracyBuffer);
for (int i = 0; i < fieldNames.length; i++)
{
data.putFloatArray(fieldNames[i], valueBuffer[i]);
}
this.transmitData(this._context, data);
for (int j = 0; j < timeBuffer.length; j++)
{
Double x = null;
Double y = null;
Double z = null;
for (int i = 0; i < fieldNames.length; i++)
{
if (fieldNames[i].equals(Continuous3DProbe.X_KEY))
x = (double) valueBuffer[i][j];
else if (fieldNames[i].equals(Continuous3DProbe.Y_KEY))
y = (double) valueBuffer[i][j];
else if (fieldNames[i].equals(Continuous3DProbe.Z_KEY))
z = (double) valueBuffer[i][j];
}
if (x != null && y != null && z != null)
{
Map<String, Object> values = new HashMap<>();
values.put(Continuous3DProbe.X_KEY, x);
values.put(Continuous3DProbe.Y_KEY, y);
values.put(Continuous3DProbe.Z_KEY, z);
values.put(ProbeValuesProvider.TIMESTAMP, timeBuffer[j] / 1000);
ProbeValuesProvider.getProvider(this._context).insertValue(this._context, GravityProbe.DB_TABLE, this.databaseSchema(), values);
}
}
bufferIndex = 0;
}
}
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy)
{
}
@Override
public String getPreferenceKey()
{
return "gravity_built_in";
}
@Override
public String summarizeValue(Context context, Bundle bundle)
{
double xReading = bundle.getDoubleArray(Continuous3DProbe.X_KEY)[0];
double yReading = bundle.getDoubleArray(Continuous3DProbe.Y_KEY)[0];
double zReading = bundle.getDoubleArray(Continuous3DProbe.Z_KEY)[0];
return String.format(context.getResources().getString(R.string.summary_gravity_probe), xReading, yReading, zReading);
}
@Override
public int getSummaryResource()
{
return R.string.summary_gravity_probe_desc;
}
@Override
protected double getThreshold()
{
SharedPreferences prefs = Probe.getPreferences(this._context);
return Double.parseDouble(prefs.getString(GravityProbe.THRESHOLD, GravityProbe.DEFAULT_THRESHOLD));
}
@Override
protected int getResourceThresholdValues()
{
return R.array.probe_accelerometer_threshold;
}
@Override
public JSONObject fetchSettings(Context context)
{
JSONObject settings = super.fetchSettings(context);
try
{
JSONObject handler = new JSONObject();
handler.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_BOOLEAN);
JSONArray values = new JSONArray();
values.put(true);
values.put(false);
handler.put(Probe.PROBE_VALUES, values);
settings.put(ContinuousProbe.USE_THREAD, handler);
}
catch (JSONException e)
{
LogManager.getInstance(context).logException(e);
}
return settings;
}
@Override
public void updateFromMap(Context context, Map<String, Object> params)
{
super.updateFromMap(context, params);
if (params.containsKey(ContinuousProbe.USE_THREAD))
{
Object handler = params.get(ContinuousProbe.USE_THREAD);
if (handler instanceof Boolean)
{
SharedPreferences prefs = Probe.getPreferences(context);
SharedPreferences.Editor e = prefs.edit();
e.putBoolean(GravityProbe.USE_HANDLER, (Boolean) handler);
e.commit();
}
}
}
}